/*******************************************************************************
 * Copyhacked (H) 2012-2025.
 * This program and the accompanying materials
 * are made available under no term at all, use it like
 * you want, but share and discuss it
 * every time possible with every body.
 * 
 * Contributors:
 *      ron190 at ymail dot com - initial implementation
 ******************************************************************************/
package com.jsql.view.swing.terminal;

import com.jsql.util.LogLevelUtil;
import com.jsql.view.swing.terminal.util.BlockCaret;
import com.jsql.view.swing.terminal.util.KeyAdapterTerminal;
import com.jsql.view.swing.util.MediatorHelper;
import com.jsql.view.swing.util.UiUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import java.awt.*;
import java.awt.event.MouseMotionListener;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A Terminal completely built from swing text pane.
 */
public abstract class AbstractExploit extends JTextPane {
    
    private static final Logger LOGGER = LogManager.getRootLogger();

    /**
     * True if terminal is processing command.
     */
    private final AtomicBoolean isEdited = new AtomicBoolean(false);

    /**
     * Server name or IP to display on prompt.
     */
    private final String host;

    /**
     * User and password for database.
     */
    private String[] loginPassword = null;
    private final UUID uuidShell;
    private final String urlShell;

    /**
     * Style used for coloring text.
     */
    private final transient Style style = this.addStyle("Necrophagist's next album is 2014.", null);

    /**
     * Length of prompt.
     */
    private String prompt = StringUtils.EMPTY;

    /**
     * Text to display next caret.
     */
    private final String labelShell;
    
    /**
     * Build a shell instance.
     * @param uuidShell Unique identifier to discriminate beyond multiple opened terminals
     * @param urlShell URL of current shell
     * @param labelShell Type of shell to display on prompt
     */
    protected AbstractExploit(UUID uuidShell, String urlShell, String labelShell) throws MalformedURLException, URISyntaxException {
        this(uuidShell, urlShell, labelShell, true);
    }
    protected AbstractExploit(UUID uuidShell, String urlShell, String labelShell, boolean isAddingPrompt) throws MalformedURLException, URISyntaxException {
        this.uuidShell = uuidShell;
        this.urlShell = urlShell;
        this.labelShell = labelShell;

        URL url;
        if (StringUtils.isEmpty(urlShell)) {  // udf
            url = new URI(MediatorHelper.model().getMediatorUtils().getConnectionUtil().getUrlByUser()).toURL();
        } else {
            url = new URI(urlShell).toURL();
        }
        this.host = url.getHost();

        this.setFont(new Font(UiUtil.FONT_NAME_MONO_NON_ASIAN, Font.PLAIN, UIManager.getFont("TextArea.font").getSize()));
        this.setCaret(new BlockCaret());
        this.setBackground(Color.BLACK);
        this.setForeground(Color.LIGHT_GRAY);

        if (isAddingPrompt) {
            this.displayPrompt(true);
        } else {
            this.append("Waiting for reverse connection...\n");
        }

        this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        this.setTransferHandler(null);
        this.setHighlighter(null);

        this.addMouseListener(new EmptyFocusCopy(this));
        this.addKeyListener(new KeyAdapterTerminal(this));
    }

    /**
     * Run when cmd is validated.
     * @param cmd Command to execute
     * @param terminalID Unique ID for terminal instance
     * @param wbhPath URL of shell
     * @param arg Additional parameters (User and password for SQLShell)
     */
    public abstract void action(String cmd, UUID terminalID, String wbhPath, String... arg);
    
    /**
     * Update terminal and use default behavior.
     */
    public void reset() {
        this.reset(true);
    }
    public void reset(boolean isPromptVisible) {
        this.isEdited.set(false);
        this.setEditable(true);
        if (isPromptVisible) {
            this.displayPrompt(false);
        }
        this.setCaretPosition(this.getDocument().getLength());
        this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
    }

    /**
     * Add a text at the end of textpane.
     * @param string Text to add
     */
    public void append(String string) {
        try {
            var doc = this.getDocument();
            doc.insertString(doc.getLength(), string, null);
        } catch (BadLocationException e) {
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
        }
    }
    
    /**
     * Append prompt to textpane, measure prompt the first time is used.
     * @param isAddingPrompt Should we measure prompt length?
     */
    public void displayPrompt(boolean isAddingPrompt) {
        StyleConstants.setUnderline(this.style, true);
        this.appendPrompt("jsql", Color.LIGHT_GRAY, isAddingPrompt);
        StyleConstants.setUnderline(this.style, false);

        this.appendPrompt(StringUtils.SPACE + this.labelShell, Color.LIGHT_GRAY, isAddingPrompt);
        this.appendPrompt("[", new Color(0x32BF32), isAddingPrompt);
        this.appendPrompt(this.host, new Color(0xBFBF19), isAddingPrompt);
        this.appendPrompt("]", new Color(0x32BF32), isAddingPrompt);
        this.appendPrompt(" >", new Color(0xBF6464), isAddingPrompt);
        this.appendPrompt(StringUtils.SPACE, Color.LIGHT_GRAY, isAddingPrompt);
    }

    /**
     * Add a colored string to the textpane, measure prompt at the same time.
     * @param string Text to append
     * @param color Color of text
     * @param isAddingPrompt Should we measure prompt length?
     */
    private void appendPrompt(String string, Color color, boolean isAddingPrompt) {
        try {
            StyleConstants.setForeground(this.style, color);
            this.getStyledDocument().insertString(this.getStyledDocument().getLength(), string, this.style);
            if (isAddingPrompt) {
                this.prompt += string;
            }
        } catch (BadLocationException e) {
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
        }
    }

    /**
     * NoWrap.
     */
    @Override
    public boolean getScrollableTracksViewportWidth() {
        return this.getUI().getPreferredSize(this).width <= this.getParent().getSize().width;
    }

    /**
     * Cancel every mouse movement processing like drag/drop.
     */
    @Override
    public synchronized void addMouseMotionListener(MouseMotionListener l) {
        // Do nothing
    }

    /**
     * Get index of line for current offset (generally cursor position).
     * @param offset Position on the line
     * @return Index of the line
     */
    public int getLineOfOffset(int offset) throws BadLocationException {
        var errorMsg = "Can't translate offset to line";
        var doc = this.getDocument();
        
        if (offset < 0) {
            throw new BadLocationException(errorMsg, -1);
        } else if (offset > doc.getLength()) {
            throw new BadLocationException(errorMsg, doc.getLength() + 1);
        } else {
            var map = doc.getDefaultRootElement();
            return map.getElementIndex(offset);
        }
    }

    /**
     * Get position of the beginning of the line.
     * @param line Index of the line
     * @return Offset of line
     */
    public int getLineStartOffset(int line) throws BadLocationException {
        var map = this.getDocument().getDefaultRootElement();
        
        if (line < 0) {
            throw new BadLocationException("Negative line", -1);
        } else if (line >= map.getElementCount()) {
            throw new BadLocationException("No such line", this.getDocument().getLength() + 1);
        } else {
            var lineElem = map.getElement(line);
            return lineElem.getStartOffset();
        }
    }

    
    // Getter and setter
    
    public AtomicBoolean getIsEdited() {
        return this.isEdited;
    }

    public UUID getUuidShell() {
        return this.uuidShell;
    }

    public String getUrlShell() {
        return this.urlShell;
    }

    public String getPrompt() {
        return this.prompt;
    }

    public String[] getLoginPassword() {
        return this.loginPassword;
    }

    public void setLoginPassword(String[] loginPassword) {
        this.loginPassword = loginPassword;
    }
}
